""" ResourceManager pooling for DCOracle2
    This module implements thread-safe (and not) connection pools.
"""
# Maciej Wisniowski - based on psycopg implementation
# psycopg/pool.py - pooling code for DCOracle2
#
# Copyright (C) 2003-2004 Federico Di Gregorio  <fog@debian.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.

import DCOracle2
from DCOracle2 import dco2
import sys
from zExceptions import BadRequest
from Products.PythonScripts.standard import html_quote
import logging
logger = logging.getLogger('DCOracle2 - pool_base.py')

class PoolError(dco2.Error):
    pass


class AbstractConnectionPool(object):
    """Generic key-based pooling code."""

    def __init__(self, minconn, maxconn, factory, *args, **kwargs):
        """Initialize the connection pool.

        New 'minconn' connections are created immediately calling 'connfunc'
        with given parameters. The connection pool will support a maximum of
        about 'maxconn' connections.
        """
        self.minconn = minconn
        self.maxconn = maxconn
        self.closed = False

        self._factory = factory
        self._args = args
        self._kwargs = kwargs

        self._pool = []
        self._used = {}
        self._rused = {} # id(conn) -> key map
        self._keys = 0

        logger.info('Creating pool of size %s for: %s' % (self.minconn, args))

        for i in range(self.minconn):
            self._connect()

    def _connect(self, key=None):
        """Create a new ResourceManager and assign it to 'key' if not None."""
        #conn = psycopg2.connect(*self._args, **self._kwargs)
        try:
            try:
                # creating ResourceManager
                DB = self._factory()
                rm = DB(*self._args, **self._kwargs)
                #conn = DCOracle2.connect(*self._args, **self._kwargs)  #self.connection_string)
                #logger.debug('_getconn. RM created. Key: \'%s\'' % (key))
            except:
                t, v, tb = sys.exc_info()
                logger.error('_getconn. RM not created. Key: \'%s\'. Error: \'%s %s\'' % (key, t, v))
                # there is a problem so we're throwing an exception
                err_info_str = '%s %s' % (t,v)
                err_str = '''<strong>Invalid connection string: </strong><CODE>%s</CODE><br>\n
                             %s<br>
                             <!--\n%s\n%s\n-->\n'''
                raise BadRequest, (err_str
                    % (self._args and self._args[0] or self._args,
                       html_quote(err_info_str), t,v)), tb
        finally: tb=None

        if key is not None:
            self._used[key] = rm
            self._rused[id(rm)] = key
        else:
            self._pool.append(rm)
        return rm

    def _getkey(self):
        """Return a new unique key."""
        self._keys += 1
        return self._keys

    def _getconn(self, key=None):
        """Get a free ResourceManageer and assign it to 'key' if not None."""
        #logger.debug('_getconn. Key: %s' % (key))
        if self.closed: raise PoolError("ResourceManager pool is closed")
        if key is None: key = self._getkey()

        if self._used.has_key(key):
            rm = self._used[key]
            #logger.debug('Already used RM has been taken from pool. Id: \'%s\'' % (key))
            #logger.debug('rm.isOpen: \'%s\'' % rm.isOpen())
            if not rm.isOpen():
                logger.info('Recreating RM with closed db connection: %s' % (self._args))
                del(self._used[key])
                del(self._rused[id(rm)])
                rm = self._connect(key)
            return rm

        if self._pool:
            self._used[key] = rm = self._pool.pop()
            self._rused[id(rm)] = key
            #logger.debug('RM has been taken from pool and marked as used. Id: %s' % (key))
            return rm
        else:
            if len(self._used) == self.maxconn:
                raise PoolError("ResourceManager pool exausted")
            logger.info('Pool too small. Creating additional connection no.: %s for %s' % (len(self._rused)+1, self._args))
            return self._connect(key)

    def _putconn(self, rm, key=None, close=False):
        """Put away a ResourceManager."""
        if self.closed: raise PoolError("ResourceManager pool is closed")
        if key is None: key = self._rused[id(rm)]

        if not key:
            raise PoolError("trying to put unkeyed ResourceManager")

        if len(self._pool) < self.minconn and not close:
            self._pool.append(rm)
            #logger.debug('RM has been returned to pool. Id: \'%s\'' % (key))
        else:
            rm.close()
            logger.info('Freeing connection to: %s.' % (self._args))

        # here we check for the presence of key because it can happen that a
        # thread tries to put back a connection after a call to close
        if not self.closed or key in self._used:
            del self._used[key]
            del self._rused[id(rm)]

    def _closeall(self):
        """Close all ResourceManagers.

        Note that this can lead to some code fail badly when trying to use
        an already closed connection. If you call .closeall() make sure
        your code can deal with it.
        """
        if self.closed: raise PoolError("ResourceManager pool is closed")
        for rm in self._pool + list(self._used.values()):
            try:
                print "Closing ResourceManager", rm
                rm.close()
            except:
                pass
        self.closed = True


class SimpleConnectionPool(AbstractConnectionPool):
    """A connection pool that can't be shared across different threads."""

    getconn = AbstractConnectionPool._getconn
    putconn = AbstractConnectionPool._putconn
    closeall   = AbstractConnectionPool._closeall


class ThreadedConnectionPool(AbstractConnectionPool):
    """A connection pool that works with the threading module."""

    def __init__(self, minconn, maxconn, factory, *args, **kwargs):
        """Initialize the threading lock."""
        import threading
        AbstractConnectionPool.__init__(
            self, minconn, maxconn, factory, *args, **kwargs)
        self._lock = threading.Lock()

    def getconn(self, key=None):
        """Get a free connection and assign it to 'key' if not None."""
        self._lock.acquire()
        try:
            return self._getconn(key)
        finally:
            self._lock.release()

    def putconn(self, rm=None, key=None, close=False):
        """Put away an unused connection."""
        self._lock.acquire()
        try:
            self._putconn(rm, key, close)
        finally:
            self._lock.release()

    def closeall(self):
        """Close all connections (even the one currently in use.)"""
        self._lock.acquire()
        try:
            self._closeall()
        finally:
            self._lock.release()


class PersistentConnectionPool(AbstractConnectionPool):
    """A pool that assigns persistent connections to different threads.

    Note that this connection pool generates by itself the required keys
    using the current thread id.  This means that untill a thread put away
    a connection it will always get the same connection object by successive
    .getconn() calls. This also means that a thread can't use more than one
    single connection from the pool.
    """

    def __init__(self, minconn, maxconn, factory, *args, **kwargs):
        """Initialize the threading lock."""
        import threading
        AbstractConnectionPool.__init__(
            self, minconn, maxconn, factory, *args, **kwargs)
        self._lock = threading.Lock()

        # we we'll need the thread module, to determine thread ids, so we
        # import it here and copy it in an instance variable
        import thread
        self.__thread = thread

    def getconn(self):
        """Generate thread id and return a connection."""
        key = self.__thread.get_ident()
        #logger.debug('getconn. identyfikator polaczenia w puli: %s' % (key))
        self._lock.acquire()
        try:
            return self._getconn(key)
        finally:
            self._lock.release()

    def putconn(self, rm=None, close=False):
        """Put away an unused connection."""
        key = self.__thread.get_ident()
        self._lock.acquire()
        try:
            if not rm: rm = self._used[key]
            self._putconn(rm, key, close)
        finally:
            self._lock.release()

    def closeall(self):
        """Close all connections (even the one currently in use.)"""
        self._lock.acquire()
        try:
            self._closeall()
        finally:
            self._lock.release()
